/*
 * License GNU LGPL
 * Copyright (C) 2012 Amrullah <amrullah@panemu.com>.
 */
package com.abc.cheque.ui.table;


import java.lang.reflect.InvocationTargetException;

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.image.ImageView;
import javafx.util.Callback;

import org.apache.commons.beanutils.NestedNullException;
import org.apache.commons.beanutils.PropertyUtils;

import com.abc.cheque.common.TableCriteria;
import com.abc.cheque.common.TiwulFXUtil;

/**
 * This is a parent class for columns that display value from a POJO object.
 * There are two variations of concrete column implementation in regards with cell editor:
 * <ul>
 * <li>Cell editor is a single control: TextField, CheckBox</li>
 * <li>Cell editor is a composite of controls: ComboBox, DateField, LookupField. They are composed from a TextField and a button
 * </ul>
 * 
 * Please refer to {@link TextColumn} and {@link ComboBoxColumn} source code to get reference on building
 * your own column.
 * 
 * Column that doesn't extend BaseColumn, i.e: {@link TickColumn} will be skipped by export-to-excel and paste routine
 * @author amrullah
 */
public class BaseColumn<S, T> extends TableColumn<S, T> {

    /**
     * used to get/set object method using introspection
     */
    private final String propertyName;
    private final SimpleObjectProperty<TableCriteria> tableCriteria = new SimpleObjectProperty<>();
    private T searchValue;
    private ImageView filterImage = new ImageView("images/filter.png");
    protected Pos alignment = Pos.CENTER_LEFT;

    /**
     *
     * @param propertyName java bean property name to be used for get/set method
     * using introspection
     * @param prefWidth preferred collumn width
     */
    public BaseColumn(String propertyName, double prefWidth) {
        this(propertyName, prefWidth, TiwulFXUtil.getLiteral(propertyName));
    }

    /**
     *
     * @param propertyName java bean property name to be used for get/set method
     * using introspection
     * @param prefWidth preferred collumn width
     * @param columnHeader column header text. Default equals propertyName. This
     * text is localized
     */
    public BaseColumn(String propertyName, double prefWidth, String columnHeader) {
        super(columnHeader);
        setPrefWidth(prefWidth);
        this.propertyName = propertyName;
//        setCellValueFactory(new PropertyValueFactory<S, T>(propertyName));
        tableCriteria.addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable observable) {
                if (tableCriteria.get() != null) {
                    BaseColumn.this.setGraphic(filterImage);
                } else {
                    BaseColumn.this.setGraphic(null);
                }
            }
        });
        setCellValueFactory(new Callback<CellDataFeatures<S, T>, ObservableValue<T>>() {
            private SimpleObjectProperty<T> propertyValue;

            @Override
            public ObservableValue<T> call(CellDataFeatures<S, T> param) {
                /**
                 * This code is adapted from {@link PropertyValueFactory#getCellDataReflectively(T)
                 */
                try {
                    Object cellValue;
                    if (getPropertyName().contains(".")) {
                        cellValue = PropertyUtils.getNestedProperty(param.getValue(), getPropertyName());
                    } else {
                        cellValue = PropertyUtils.getSimpleProperty(param.getValue(), getPropertyName());
                    }
                    propertyValue = new SimpleObjectProperty<>((T) cellValue);
                    return propertyValue;
                } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
                    throw new RuntimeException(ex);
                } catch (NestedNullException ex) {
                    return new SimpleObjectProperty<>(null);
                }
            }
        });
    }

    /**
     * Property that holds applied criteria to column
     *
     * @return
     */
    public SimpleObjectProperty<TableCriteria> tableCriteriaProperty() {
        return tableCriteria;
    }

    /**
     * Get criteria applied to this column
     *
     * @return
     * @see #tableCriteriaProperty()
     */
    public TableCriteria getTableCriteria() {
        return tableCriteria.get();
    }

    /**
     * Set criteria to be applied to column. If you are going to set criteria
     * to multiple columns, it is encouraged to call {@link TableControl#setReloadOnCriteriaChange(boolean) }
     * and pass FALSE as parameter. It will disable autoreload on criteria change. After assign all
     * criteria, call {@link TableControl#reloadFirstPage()}. You might want to set {@link TableControl#setReloadOnCriteriaChange(boolean) } back to true
     * after that.
     *
     * @param crit
     * @see TableControl#setReloadOnCriteriaChange(boolean)
     */
    public void setTableCriteria(TableCriteria crit) {
        tableCriteria.set(crit);
    }

    /**
     * Gets propertyName passed in constructor
     * @return 
     */
    public String getPropertyName() {
        return propertyName;
    }

    /**
     * Get Search Menu Item that is displayed in Table context menu
     *
     * @return
     */
    MenuItem getSearchMenuItem() {
        return null;
    }

    void setDefaultSearchValue(T searchValue) {
        this.searchValue = searchValue;
    }

    T getDefaultSearchValue() {
        return searchValue;
    }

    /**
     * Gets cell alignment
     * @return 
     */
    public Pos getAlignment() {
        return alignment;
    }

    /**
     * Sets cell alignment
     * @param alignment 
     */
    public void setAlignment(Pos alignment) {
        this.alignment = alignment;
    }
    
    private BooleanProperty filterable = new SimpleBooleanProperty(true);

    public BooleanProperty filterableProperty() {
        return filterable;
    }

    /**
     * Specifies whether right clicking the column will show menu item to do
     * filtering. If filterable is true, search menu item will be displayed in
     * context menu.
     */
    public void setFilterable(boolean filterable) {
        this.filterable.set(filterable);
    }

    public boolean isFilterable() {
        return this.filterable.get();
    }
    
    private BooleanProperty required = new SimpleBooleanProperty(false);
    /**
     * Set the field to required and cannot null. Some columns implementation
     * provide empty value that user can select if the column is not required.
     * 
     * @param required 
     */
    public void setRequired(boolean required) {
        this.required.set(required);
    }

    public boolean isRequired() {
        return required.get();
    }
    
    public BooleanProperty requiredProperty() {
        return required;
    }
    
    /**
     * Convert <code>stringValue</code> to value that is acceptable
     * by this column
     * @param stringValue
     * @return 
     */
    public final T convertFromString(String stringValue) {
        T result = convertFromString_Impl(stringValue);
        if (result == null && stringValue != null && !stringValue.isEmpty()) {
            throw new RuntimeException("Unable to find object from \""+stringValue+"\" string representation");
        }
        return (T) result;
    }
    
    protected T convertFromString_Impl (String stringValue) {
        return (T) stringValue;
    }
    
    /**
     * Convert <code>value</code> to String as represented in TableControl
     * @param value
     * @return 
     */
    public String convertToString(T value) {
        if (value == null) {
            return null;
        }
        return value.toString();
    }
}
